﻿using System;
using TDD.SpreadSheet.Exceptions;

namespace TDD.SpreadSheet
{
    public class Spreadsheet
    {
        #region Constructors
        public Spreadsheet()
            : this("ZZ", 999)
        {
        }

        public Spreadsheet(string lastColumn, int lastRow)
        {
            if (!Cell.IsValidColumn(lastColumn))
            {
                throw new ArgumentException(string.Format("The column {0} is not a valid column format.", lastColumn), "lastColumn");
            }
            SpreadsheetCells = new Cell[lastRow, Cell.GetColumNumber(lastColumn)];
        }
        #endregion

        #region Properties
        private Cell[,] SpreadsheetCells
        { get; set; }
        #endregion

        #region Helper functions
        private Cell GetValidCell(string cell)
        {
            if (SpreadsheetCells == null)
            {
                throw new Exception("Spreadsheet is empty");
            }
            int row;
            int column;
            Cell.SplitColumnAndRow(cell, out column, out row);

            try
            {
                Cell validCell = SpreadsheetCells[row, column];

                if (validCell == null)
                {
                    validCell = new Cell(cell);
                    SpreadsheetCells[row, column] = validCell;
                }

                return validCell;
            }
            catch (IndexOutOfRangeException ex)
            {
                throw new ArgumentException("Cell is out of range", ex);
            }
            catch (Exception ex)
            {
                throw new ArgumentException("Cell is not a valid cell", ex);
            }
        }

        private void SetValue(string value, Cell validCell)
        {
            validCell.Literal = value;

            //try to set int value
            int auxInt;
            if (int.TryParse(value, out auxInt))
            {
                validCell.Value = auxInt.ToString();
                return;
            }

            //try to set decimal value
            decimal auxDec;
            if (decimal.TryParse(value, out auxDec))
            {
                validCell.Value = auxDec.ToString();
                return;
            }

            if (value.Length > 0)
            {
                if (value.StartsWith("=") && value.Length > 1)
                {
                    //the data is a formula
                    decimal valueDec = Convert.ToDecimal(Calculate(value.Substring(1), validCell));
                    validCell.Value = valueDec.ToString();
                }
                else
                {
                    //the data is a string value
                    validCell.Value = value;
                }
            }
            else
            {
                //the data is a string value
                validCell.Value = value;
            }
        }

        private string Calculate(string literal, Cell currentCell)
        {
            string beginLiteral = string.Empty;

            bool haveCalculated = false;
            int parenthesisCount = 0;
            int positionOpenParenthesis = 0;
            while (true)
            {
                for (int i = 0; i < literal.Length; i++)
                {
                    if (literal.ToCharArray()[i] == '(')
                    {
                        if (parenthesisCount == 0)
                        {
                            positionOpenParenthesis = i + 1;
                        }
                        parenthesisCount++;
                    }
                    else if (literal.ToCharArray()[i] == ')')
                    {
                        parenthesisCount--;

                        if (parenthesisCount < 0)
                        {
                            throw new InvalidFormulaException("There is a mistake in the formula.");
                        }
                        if (parenthesisCount == 0)
                        {
                            beginLiteral = literal.Substring(0, positionOpenParenthesis - 1);
                            string result = Calculate(literal.Substring(positionOpenParenthesis, i - positionOpenParenthesis), currentCell);
                            literal = beginLiteral + result + literal.Substring(i + 1);
                            haveCalculated = true;
                            break;
                        }
                    }
                }
                if (!haveCalculated)
                {
                    break;
                }
                else
                {
                    haveCalculated = false;
                }
            }
            if (parenthesisCount != 0)
            {
                throw new InvalidFormulaException("There is a mistake in the formula.");
            }

            int postionSeparator = 0;
            int positionCurrentLevelOperator = 0;
            for (int j = 1; j <= 2; j++)
            {
                while (true)
                {
                    for (int i = 0; i < literal.Length; i++)
                    {
                        if (Operators.IsOperator(literal.ToCharArray()[i]))
                        {
                            if (Operators.GetOperatorOrder(literal.ToCharArray()[i]) == j)
                            {
                                if (positionCurrentLevelOperator != 0)
                                {
                                    //calculate
                                    string valueA = string.Empty;
                                    string valueB = string.Empty;
                                    beginLiteral = string.Empty;
                                    if (postionSeparator != 0)
                                    {
                                        beginLiteral = literal.Substring(0, postionSeparator + 1);
                                        valueA = GetValidStringToCalculate(literal.Substring(postionSeparator + 1, positionCurrentLevelOperator - postionSeparator - 1), currentCell);
                                        valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1, i - positionCurrentLevelOperator - 1), currentCell);
                                    }
                                    else
                                    {
                                        valueA = GetValidStringToCalculate(literal.Substring(postionSeparator, positionCurrentLevelOperator), currentCell);
                                        valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1, i - positionCurrentLevelOperator - 1), currentCell);
                                    }
                                    string result = new Operator(literal.ToCharArray()[positionCurrentLevelOperator]).Calculate(Convert.ToDecimal(valueA), Convert.ToDecimal(valueB)).ToString();
                                    literal = beginLiteral + result + literal.Substring(i);
                                    haveCalculated = true;
                                    positionCurrentLevelOperator = 0;
                                    break;
                                }
                                positionCurrentLevelOperator = i;
                            }
                            else
                            {
                                if (positionCurrentLevelOperator != 0)
                                {
                                    //calculate
                                    string valueA = string.Empty;
                                    string valueB = string.Empty;
                                    beginLiteral = string.Empty;
                                    if (postionSeparator != 0)
                                    {
                                        beginLiteral = literal.Substring(0, postionSeparator + 1);
                                        valueA = GetValidStringToCalculate(literal.Substring(postionSeparator + 1, positionCurrentLevelOperator - postionSeparator - 1), currentCell);
                                        valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1, i - positionCurrentLevelOperator - 1), currentCell);
                                    }
                                    else
                                    {
                                        valueA = GetValidStringToCalculate(literal.Substring(postionSeparator, positionCurrentLevelOperator), currentCell);
                                        valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1, i - positionCurrentLevelOperator - 1), currentCell);
                                    }
                                    string result = new Operator(literal.ToCharArray()[positionCurrentLevelOperator]).Calculate(Convert.ToDecimal(valueA), Convert.ToDecimal(valueB)).ToString();
                                    literal = beginLiteral + result + literal.Substring(i);
                                    haveCalculated = true;
                                    positionCurrentLevelOperator = 0;
                                    break;
                                }
                                postionSeparator = i;
                            }
                        }
                    }
                    if (!haveCalculated)
                    {
                        if (positionCurrentLevelOperator != 0)
                        {
                            //calculate
                            string valueA = string.Empty;
                            string valueB = string.Empty;
                            beginLiteral = string.Empty;
                            if (postionSeparator != 0)
                            {
                                beginLiteral = literal.Substring(0, postionSeparator + 1);
                                valueA = GetValidStringToCalculate(literal.Substring(postionSeparator + 1, positionCurrentLevelOperator - postionSeparator - 1), currentCell);
                                valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1), currentCell);
                            }
                            else
                            {
                                valueA = GetValidStringToCalculate(literal.Substring(postionSeparator, positionCurrentLevelOperator), currentCell);
                                valueB = GetValidStringToCalculate(literal.Substring(positionCurrentLevelOperator + 1), currentCell);
                            }

                            string result = new Operator(literal.ToCharArray()[positionCurrentLevelOperator]).Calculate(Convert.ToDecimal(valueA), Convert.ToDecimal(valueB)).ToString();
                            literal = beginLiteral + result;
                        }
                        positionCurrentLevelOperator = 0;
                        postionSeparator = 0;
                        break;
                    }
                    else
                    {
                        haveCalculated = false;
                    }
                }
            }

            return GetValidStringToCalculate(literal, currentCell);
        }

        private string GetValidStringToCalculate(string value, Cell currentCell)
        {
            if (value.Trim() == string.Empty)
            {
                throw new InvalidFormulaException("There is a mistake in the formula.");
            }

            decimal auxDec = 0;
            if (decimal.TryParse(value, out auxDec))
            {
                return auxDec.ToString();
            }

            Cell validCell;
            try
            {
                //if the formula is wrong, the cell will not found
                validCell = GetValidCell(value.Trim());
            }
            catch (ArgumentException)
            {
                throw new InvalidFormulaException("There is a mistake in the formula.");
            }

            if (validCell == null)
            {
                throw new InvalidFormulaException("There is a mistake in the formula.");
            }
            if (validCell.Name == currentCell.Name)
            {
                throw new CircularReferenceException();
            }
            //the currentCell is added to this cell as reference cell
            //if the value of this cell changes, the value
            //of currentCell should be change too
            if (!validCell.RelatedCells.Contains(currentCell))
            {
                validCell.RelatedCells.Add(currentCell);
            }
            string auxValue = validCell.Literal.StartsWith("=") ? Calculate(validCell.Literal.Substring(1), validCell) : validCell.Literal;
            if (auxValue == Cell.SpecialValues.ERROR)
            {
                throw new InvalidFormulaException("There is a mistake in the formula.");
            }
            else if (auxValue == Cell.SpecialValues.CIRCULAR)
            {
                throw new CircularReferenceException();
            }
            else if (auxValue == string.Empty)
            {
                //We are working with formulas so, We have to return a decimal default value 
                auxValue = Cell.SpecialValues.DECIMAL_DEFAULT_VALUE;
            }
            return auxValue;
        }
        #endregion

        #region Public Functions
        public string Get(string cell)
        {
            return GetValidCell(cell).Value;
        }

        public string GetLiteral(string cell)
        {
            return GetValidCell(cell).Literal;
        }

        public void Put(String cell, string value)
        {
            if (value == null)
            {
                throw new ArgumentException("The cell value can not be null.", "value");
            }
            Cell validCell = GetValidCell(cell);
            try
            {
                SetValue(value, validCell);
            }
            catch (InvalidFormulaException)
            {
                SetValue(Cell.SpecialValues.ERROR, validCell);
            }
            catch (CircularReferenceException)
            {
                SetValue(Cell.SpecialValues.CIRCULAR, validCell);
            }
            CalculateRelatedCellValues(validCell);
        }

        private void CalculateRelatedCellValues(Cell currentCell)
        {
            foreach (Cell relatedCell in currentCell.RelatedCells)
            {
                Put(relatedCell.Name, relatedCell.Literal);
            }
        }
        #endregion
    }
}
